#!/bin/bash
#
# Copyright 2021 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

init_globals() {
    if [ "$0" != "/bin/bash" ]; then
        SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
        export RUN_SCRIPT_FILE="$(readlink -f "$0")"
    else
        export RUN_SCRIPT_FILE="$(readlink -f "${BASH_SOURCE[0]}")"
    fi

    export TOP=$(git rev-parse --show-toplevel || $(dirname "${RUN_SCRIPT_FILE}"))
    MONAI_PY_EXE=${MONAI_PY_EXE:-"python3"}
    export MONAI_PY_EXE

    DO_DRY_RUN="false"  # print commands but do not execute them. Used by run_command
}

################################################################################
# Utility functions
################################################################################

#######################################
# Get list of available commands from a given input file.
#
# Available commands and command summary are extracted by checking a pattern
# "_desc() { c_echo '".
# Section title is extracted by checking a pattern "# Section: ".
# This command is used for listing available commands in CLI.
#
# e.g.)
#   "# Section: String/IO functions"
#     => "# String/IO functions"
#   "to_lower_desc() { c_echo 'Convert to lower case"
#     => "to_lower ----------------- Convert to lower case"
#
# Arguments:
#   $1 - input file that defines commands
# Returns:
#   Print list of available commands from $1
#######################################
get_list_of_available_commands() {
    local mode="color"
    if [ "${1:-}" = "color" ]; then
        mode="color"
        shift
    elif [ "${1:-}" = "nocolor" ]; then
        mode="nocolor"
        shift
    fi

    local file_name="$1"
    if [ ! -e "$1" ]; then
        echo "$1 doesn't exist!"
    fi

    local line_str='--------------------------------'
    local IFS= cmd_lines="$(IFS= cat "$1" | grep -E -e "^(([[:alpha:]_[:digit:]]+)_desc\(\)|# Section: )" | sed "s/_desc() *{ *c_echo '/ : /")"
    local line
    while IFS= read -r line; do
        local cmd=$(echo "$line" | cut -d":" -f1)
        local desc=$(echo "$line" | cut -d":" -f2-)
        if [ "$cmd" = "# Section" ]; then
            c_echo ${mode} B "${desc}"
        else
            # there is no substring operation in 'sh' so use 'cut'
            local dash_line="$(echo "${line_str}" | cut -c ${#cmd}-)"  #  = "${line_str:${#cmd}}"
             c_echo ${mode} Y "   ${cmd}" w " ${dash_line} ${desc}"
        fi
        # use <<EOF, not '<<<"$cmd_lines"' to be executable in sh
    done <<EOF
$cmd_lines
EOF
}

my_cat_prefix() {
    local IFS
    local prefix="$1"
    local line
    while IFS= read -r line; do
        echo "${prefix}${line}" # -e option doesn't work in 'sh' so disallow escaped characters
    done <&0
}

c_str() {
    local old_color=39
    local old_attr=0
    local color=39
    local attr=0
    local text=""
    local mode="color"
    if [ "${1:-}" = "color" ]; then
        mode="color"
        shift
    elif [ "${1:-}" = "nocolor" ]; then
        mode="nocolor"
        shift
    fi

    for i in "$@"; do
        case "$i" in
            r|R)
                color=31
                ;;
            g|G)
                color=32
                ;;
            y|Y)
                color=33
                ;;
            b|B)
                color=34
                ;;
            p|P)
                color=35
                ;;
            c|C)
                color=36
                ;;
            w|W)
                color=37
                ;;

            z|Z)
                color=0
                ;;
        esac
        case "$i" in
            l|L|R|G|Y|B|P|C|W)
                attr=1
                ;;
            n|N|r|g|y|b|p|c|w)
                attr=0
                ;;
            z|Z)
                attr=0
                ;;
            *)
                text="${text}$i"
        esac
        if [ "${mode}" = "color" ]; then
            if [ ${old_color} -ne ${color} ] || [ ${old_attr} -ne ${attr} ]; then
                text="${text}\033[${attr};${color}m"
                old_color=$color
                old_attr=$attr
            fi
        fi
    done
    /bin/echo -en "$text"
}

c_echo() {
    # Select color/nocolor based on the first argument
    local mode="color"
    if [ "${1:-}" = "color" ]; then
        mode="color"
        shift
    elif [ "${1:-}" = "nocolor" ]; then
        mode="nocolor"
        shift
    else
        if [ ! -t 1 ]; then
            mode="nocolor"
        fi
    fi

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +x # unset xtrace

    if [ "${mode}" = "color" ]; then
        local text="$(c_str color "$@")"
        /bin/echo -e "$text\033[0m"
    else
        local text="$(c_str nocolor "$@")"
        /bin/echo -e "$text"
    fi
    eval "${old_opt}" # restore old xtrace option
}

echo_err() {
    >&2 echo "$@"
}

c_echo_err() {
    >&2 c_echo "$@"
}

printf_err() {
    >&2 printf "$@"
}

get_item_ranges() {
    local indexes="$1"
    local list="$2"
    echo -n "$(echo "${list}" | xargs | cut -d " " -f "${indexes}")"
    return $?
}

get_unused_ports() {
    local num_of_ports=${1:-1}
    local start=${2:-49152}
    local end=${3:-61000}
    comm -23 \
    <(seq ${start} ${end} | sort) \
    <(ss -tan | awk '{print $4}' | while read line; do echo ${line##*\:}; done | grep '[0-9]\{1,5\}' | sort -u) \
    | shuf | tail -n ${num_of_ports} # use tail instead head to avoid broken pipe in VSCode terminal
}

newline() {
    echo
}

info() {
    c_echo W "$(date -u '+%Y-%m-%d %H:%M:%S') [INFO] " Z "$@"
}

error() {
    echo R "$(date -u '+%Y-%m-%d %H:%M:%S') [ERROR] " Z "$@"
}

fatal() {
    echo R "$(date -u '+%Y-%m-%d %H:%M:%S') [FATAL] " Z "$@"
    echo
    if [ -n "${SCRIPT_DIR}" ]; then
        exit 1
    fi
}

run_command() {
    local status=0
    local cmd="$*"

    if [ "${DO_DRY_RUN}" != "true" ]; then
        c_echo B "$(date -u '+%Y-%m-%d %H:%M:%S') " W "\$ " G "${cmd}"
    else
        c_echo B "$(date -u '+%Y-%m-%d %H:%M:%S') " C "[dryrun] " W "\$ " G "${cmd}"
    fi

    [ "$(echo -n "$@")" = "" ] && return 1 # return 1 if there is no command available

    if [ "${DO_DRY_RUN}" != "true" ]; then
        "$@"
        status=$?
    fi

    return $status
}

retry() {
    local retries=$1
    shift

    local count=0
    until run_command "$@"; do
        exit=$?
        wait=$((2 ** count))
        count=$((count + 1))
        if [ $count -lt $retries ]; then
            info "Retry $count/$retries. Exit code=$exit, Retrying in $wait seconds..."
            sleep $wait
        else
            fatal "Retry $count/$retries. Exit code=$exit, no more retries left."
            return 1
        fi
    done
    return 0
}

#==================================================================================
# Section: Inspect
#==================================================================================

get_package_info_desc() { c_echo 'Get package info from app(& model) path

Arguments:
  $1 - Application folder or file path
  $2 - Model path (optional)
'
}
get_package_info() {
    local app_path="${1:-}"
    local model_path="${2:-}"

    [ -z "${app_path}" ] && c_echo_err R "No application path specified." && return 1

    ${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py get_package_info ${app_path} ${model_path}
}

#==================================================================================
# Section: Build
#==================================================================================

install_python_dev_deps() {
    local config="${1:-dev}"
    local is_read_the_docs="false"
    if [ -n "${VIRTUAL_ENV}" ] || [ -n "${CONDA_PREFIX}" ]; then
        # Read the Docs site is using specific setuptools version so should not upgrade it.
        if [ "$config" = "read_the_docs" ]; then
            is_read_the_docs="true"
        else
            run_command ${MONAI_PY_EXE} -m pip install -q -U setuptools pip wheel build
        fi
        run_command ${MONAI_PY_EXE} -m pip install -q -r ${TOP}/requirements-dev.txt
        run_command ${MONAI_PY_EXE} -m pip install -q -r ${TOP}/requirements-examples.txt
    else
        c_echo_err R "You must be in a virtual environment to install dependencies."
        if [ ! -e "$TOP/.venv/dev/bin/python3" ]; then
            c_echo_err W "Installing a virtual environment at " G "$TOP/.venv/dev" W " ..."
            run_command ${MONAI_PY_EXE} -m venv "$TOP/.venv/dev"
        fi

        c_echo_err W "Please activate the virtual environment at " G "$TOP/.venv/dev" W " and execute the setup command again."
        c_echo_err
        c_echo_err G "  source $TOP/.venv/dev/bin/activate"
        c_echo_err G "  $0 $CMD $ARGS"
        exit 1
    fi

    # Adding temp fix to address the issue of holoscan sdk dragging in low level dependencies, e.g. libcuda.so
    # fix_holoscan_import

    install_edit_mode

    # Install packages overridden by Holoscan package if readthedocs is enabled
    if [ ${is_read_the_docs} = "true" ]; then
        # Upgrade pip overridden by Holoscan
        run_command ${MONAI_PY_EXE} -m pip install -q -U pip wheel build
        # Upgrade PyYAML to avoid the issue with the version installed by Holoscan
        run_command ${MONAI_PY_EXE} -m pip install -U PyYAML

        # Install cuda runtime dependency
        run_command ${MONAI_PY_EXE} -m pip install nvidia-cuda-runtime-cu12

        # Copy the cuda runtime library to the fixed location (workaround for readthedocs) so that
        # we can leverage the existing LD_LIBRARY_PATH (configured by the readthedocs UI) to locate the cuda runtime library.
        # (LD_LIBRARY_PATH is set to /home/docs/ for that purpose)
        # Note that 'python3.9' is hard-coded here, it should be updated if the Python version changes by
        # .readthedocs.yml or other configurations.
        run_command ls -al /home/docs/checkouts/readthedocs.org/user_builds/${READTHEDOCS_PROJECT}/envs/${READTHEDOCS_VERSION}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib/
        run_command cp /home/docs/checkouts/readthedocs.org/user_builds/${READTHEDOCS_PROJECT}/envs/${READTHEDOCS_VERSION}/lib/python3.9/site-packages/nvidia/cuda_runtime/lib/*.so* /home/docs/
        run_command ls -al /home/docs/
    fi
}

fix_holoscan_import() {
    local holoscan_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path holoscan)
    c_echo b "holoscan_package_path : " Z "${holoscan_package_path}"
    init_file_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py fix_holoscan_import)
    c_echo b "done fixing file: " Z "${init_file_path}"
}

install_edit_mode() {
    # Create a symlink to the virtual environment.
    # This solves an issue with separate package folders for 'monai' and 'monai-deploy-app-sdk' like below:
    # - 'monai': installed with 'pip install monai'
    # - 'monai-deploy-app-sdk': installed with 'pip install -e .  # installed with development(edit) mode
    local monai_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path monai)
    local sdk_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path monai-deploy-app-sdk)
    local is_sdk_editable="false"
    if ${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py is_dist_editable monai-deploy-app-sdk; then
        is_sdk_editable="true"
    fi

    c_echo b "monai_package_path: " Z "${monai_package_path}"
    c_echo b "sdk_package_path  : " Z "${sdk_package_path}"
    c_echo b "is_sdk_editable   : " Z "${is_sdk_editable}"

    if [ "${is_sdk_editable}" == "false" ]; then
        c_echo W "Installing monai-deploy-app-sdk in edit mode..."
        if [ -n "${VIRTUAL_ENV}" ] || [ -n "${CONDA_PREFIX}" ]; then
            run_command ${MONAI_PY_EXE} -m pip install -e ${TOP}
        else
            c_echo_err R "You must be in a virtual environment to install monai-deploy-app-sdk in edit mode."
        fi

        if ${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py is_dist_editable monai-deploy-app-sdk; then
            is_sdk_editable="true"
        fi

        sdk_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path monai-deploy-app-sdk)
        c_echo b "sdk_package_path  : " Z "${sdk_package_path}"
        c_echo b "is_sdk_editable   : " Z "${is_sdk_editable}"
    fi

    if [ "${is_sdk_editable}" = "false" ]; then
        c_echo W "Installing monai-deploy-app-sdk in edit mode..."
    fi

    if [ "$is_sdk_editable" = "true" ] && [ -n "${monai_package_path}" ] && [ ! -L ${monai_package_path}/monai/deploy ]; then
        c_echo W "Creating a symbolic link... " b "'${monai_package_path}/monai/deploy' " Z "->" b " '${sdk_package_path}/monai/deploy'"
        run_command ln -sfn ${sdk_package_path}/monai/deploy ${monai_package_path}/monai/deploy
    fi

    # Refresh command-line tools
    hash -r
}

setup_desc() { c_echo 'Setup development environment

Arguments:
  $1 - configuration (default: "dev")
'
}
setup() {
    local config="${1:-dev}"
    install_python_dev_deps "${config}"
}

clean_desc() { c_echo 'Clean up temporary files and uninstall monai-deploy-app-sdk
'
}
clean() {
    c_echo W 'Cleaning up temporary files and run "' "${MONAI_PY_EXE}" ' -m pip uninstall monai-deploy-app-sdk"...'

    # Remove coverage history
    run_command rm -f junit-monai-deploy-app-sdk.xml monai-deploy-app-sdk-coverage.xml

    # Uninstall the development package
    if [ -n "${VIRTUAL_ENV}" ] || [ -n "${CONDA_PREFIX}" ]; then
        c_echo W "Uninstalling MONAI Deploy App SDK installation..."
        run_command ${MONAI_PY_EXE} -m pip uninstall monai-deploy-app-sdk
        run_command ${MONAI_PY_EXE} -m pip uninstall holoscan
    fi

    local monai_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path monai)
    local sdk_package_path=$(${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py dist_module_path monai-deploy-app-sdk)
    local is_sdk_editable="false"

    if ${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py is_dist_editable monai-deploy-app-sdk; then
        is_sdk_editable="true"
    fi

    c_echo b "monai_package_path: " Z "${monai_package_path}"
    c_echo b "sdk_package_path  : " Z "${sdk_package_path}"
    c_echo b "is_sdk_editable   : " Z "${is_sdk_editable}"

    if [ "$is_sdk_editable" = "false" ] && [ -n "${monai_package_path}" ] && [ -L ${monai_package_path}/monai/deploy ]; then
        c_echo W "Deleting a symbolic link at " b "'${monai_package_path}/monai/deploy'... "
        run_command rm ${monai_package_path}/monai/deploy
    fi

    # Remove temporary files (in the directory of this script)
    c_echo W "Removing temporary files in ${TOP}"
    run_command find ${TOP}/monai -type f -name "*.py[co]" -delete
    run_command find ${TOP}/monai -type f -name "*.so" -delete
    run_command find ${TOP}/monai -type d -name "__pycache__" -delete

    run_command find ${TOP} -depth -maxdepth 1 -type d -name "monai_deploy_app_sdk.egg-info" -exec rm -r "{}" +
    run_command find ${TOP} -depth -maxdepth 1 -type d -name "build" -exec rm -r "{}" +
    run_command find ${TOP} -depth -maxdepth 1 -type d -name "dist" -exec rm -r "{}" +
    run_command find ${TOP} -depth -maxdepth 1 -type d -name ".mypy_cache" -exec rm -r "{}" +
    run_command find ${TOP} -depth -maxdepth 1 -type d -name ".pytype" -exec rm -r "{}" +
    run_command find ${TOP} -depth -maxdepth 1 -type d -name "__pycache__" -exec rm -r "{}" +
}

build_desc() { c_echo 'Build distribution package

Build a distribution package for this SDK using
"build" (https://pypa-build.readthedocs.io/en/stable/index.html).

Arguments:
  $1 - destination folder (default: ${TOP}/dist)
'
}
build() {
    local dest_path="${1:-${TOP}/dist}"
    install_python_dev_deps

    run_command rm -rf ${dest_path}/monai-deploy-app-sdk-*.gz ${dest_path}/monai_deploy_app_sdk-*.whl

    # Somehow, 'build' package causes an issue without `PIP_NO_CACHE_DIR=off` at Python 3.6 (with pyenv)
    # (https://github.com/pypa/pip/issues/2897)
    PIP_NO_CACHE_DIR=off run_command ${MONAI_PY_EXE} -m build -o "${dest_path}"
}

#==================================================================================
# Section: Test
#==================================================================================

get_platform_name() {
    ${MONAI_PY_EXE} -c "import platform; print(platform.system().lower())"
}

get_python_version() {
    ${MONAI_PY_EXE} -c "from sys import version_info; print(f'{version_info.major}.{version_info.minor}')"
}

check_import() {
    [ -z "${1:-}" ] && c_echo_err R "No module specified." && return 1

    local module_name="${1:-}"
    c_echo W "Checking import of module '" b "${module_name}" W "'..."
    run_command ${MONAI_PY_EXE} -c "import ${1}"
}

is_module_installed() {
    local module_name="${1:-}"
    local result=0

    [ -z "${module_name}" ] && c_echo_err R "No module name specified." && return 1

    ${MONAI_PY_EXE} $TOP/monai/deploy/utils/importutil.py is_module_installed "${module_name}"
    result=$?
    return ${result}
}

# Edit 'setup.cfg' ([isort] section) if you want to skip formatting for particular files.
isort_format() {
    local command="${1:-}"
    local result=0

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +e # unset error trap

    if [ "${command}" = "fix" ]; then
        c_echo W "Fixing import order..."
    else
        c_echo W "Checking import order..."
    fi

    # Ensure that the necessary packages for code format testing are installed
    if ! is_module_installed isort; then
        install_python_dev_deps
    fi

    run_command ${MONAI_PY_EXE} -m isort --version

    if [ "${command}" = "fix" ]; then
        run_command ${MONAI_PY_EXE} -m isort "${TOP}"
    else
        run_command ${MONAI_PY_EXE} -m isort --check "${TOP}"
    fi
    result=$?

    if [ ${result} -ne 0 ]; then
        c_echo_err R "isort check failed!"
        c_echo_err W "Please run auto style fixes: " G "./run check --autofix"
        exit ${result}
    else
        c_echo_err G "isort check passed!"
    fi

    eval "${old_opt}" # restore old xtrace option
}

# Edit 'pyproject.toml' ([tool.black] section) if you want to skip formatting for particular files.
black_format() {
    local command="${1:-}"
    local result=0

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +e # unset error trap

    if [ "${command}" = "fix" ]; then
        c_echo W "Fixing styles using black..."
    else
        c_echo W "Checking styles using black..."
    fi

    # Ensure that the necessary packages for code format testing are installed
    if ! is_module_installed black; then
        install_python_dev_deps
    fi

    run_command ${MONAI_PY_EXE} -m black --version

    if [ "${command}" = "fix" ]; then
        run_command ${MONAI_PY_EXE} -m black "${TOP}"
    else
        run_command ${MONAI_PY_EXE} -m black --check "${TOP}"
    fi
    result=$?

    if [ ${result} -ne 0 ]; then
        c_echo_err R "black check failed!"
        c_echo_err W "Please run auto style fixes: " G "./run check --autofix"
        exit ${result}
    else
        c_echo_err G "black check passed!"
    fi

    eval "${old_opt}" # restore old xtrace option
}

# Edit 'setup.cfg' ([flake8] section) if you want to skip formatting for particular files.
flake8_format() {
    local command="${1:-}"
    local result=0

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +e # unset error trap

    c_echo W "Checking styles using flake8..."

    # Ensure that the necessary packages for code format testing are installed
    if ! is_module_installed flake8; then
        install_python_dev_deps
    fi

    run_command ${MONAI_PY_EXE} -m flake8 --version

    run_command ${MONAI_PY_EXE} -m flake8 "${TOP}" --count --statistics
    result=$?

    if [ ${result} -ne 0 ]; then
        c_echo_err R "flake8 check failed!"
        exit ${result}
    else
        c_echo_err G "flake8 check passed!"
    fi

    eval "${old_opt}" # restore old xtrace option
}

# Edit 'setup.cfg' ([pytype] section) if you want to skip formatting for particular files.
# You can use '# type: ignore' to ignore a specific line in a file.
pytype_format() {
    local num_workers="${1:-$(nproc)}"
    local result=0

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +e # unset error trap

    c_echo W "Checking styles using pytype..."

    local platform_name="$(get_platform_name)"

    # Skip if it is MacOS
    if [ "${platform_name}" = "darwin" ]; then
        c_echo_err R "pytype does not work on MacOS (https://github.com/google/pytype/issues/661), skipping the checking."
        return 0
    fi

    # Ensure that the necessary packages for code format testing are installed
    if ! is_module_installed pytype; then
        install_python_dev_deps
    fi

    run_command ${MONAI_PY_EXE} -m pytype --version

    local python_version="$(get_python_version)"

    pushd "${TOP}" > /dev/null
    run_command ${MONAI_PY_EXE} -m pytype -j "${num_workers}" --python-version="${python_version}"
    result=$?
    popd > /dev/null

    if [ ${result} -ne 0 ]; then
        c_echo_err R "pytype check failed!"
        exit ${result}
    else
        c_echo_err G "pytype check passed!"
    fi

    eval "${old_opt}" # restore old xtrace option
}

# Edit 'setup.cfg' ([mypy] section) if you want to skip formatting for particular files.
# You can use '# type: ignore' to ignore a specific line in a file.
mypy_format() {
    local command="${1:-}"
    local result=0

    local old_opt="$(shopt -op xtrace)" # save old xtrace option
    set +e # unset error trap

    c_echo W "Checking styles using mypy..."

    local platform_name="$(get_platform_name)"

    # Ensure that the necessary packages for code format testing are installed
    if ! is_module_installed mypy; then
        install_python_dev_deps
    fi

    run_command ${MONAI_PY_EXE} -m mypy --version

    local python_version="$(get_python_version)"
    #MYPYPATH="${TOP}/monai/deploy"

    # https://github.com/python/mypy/issues/8584: Redesign import handling
    # https://github.com/python/mypy/issues/10172: Issue with `pip install -e .`
    # - https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-explicit-package-bases
    # - https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-paths-to-modules
    #   - Using `--namespace-packages --explicit-package-bases` solves the issue with the current folder structure.
    run_command ${MONAI_PY_EXE} -m mypy --namespace-packages --explicit-package-bases "${TOP}"
    result=$?

    if [ ${result} -ne 0 ]; then
        c_echo_err R "mypy check failed!"
        exit ${result}
    else
        c_echo_err G "mypy check passed!"
    fi

    eval "${old_opt}" # restore old xtrace option
}

check_desc() { c_echo 'Check code quality

Examples:
./run check -f                      # run coding style and static type checking.
./run check --autofix               # run automatic code formatting using "isort" and "black".

Code style check options:
    --black           : perform "black" code format checks
    --autofix         : format code using "isort" and "black"
    --isort           : perform "isort" import sort checks
    --flake8          : perform "flake8" code format checks

Python type check options:
    --pytype          : perform "pytype" static type checks
    --mypy            : perform "mypy" static type checks
    -j, --jobs        : number of parallel jobs to run "pytype" (default: ' $(nproc) ')

Misc. options:
    --dryrun          : display the commands to the screen without running
    -f, --codeformat  : shorthand to run all code style and static analysis tests

--------------------------------------------------------------------------------
For bug reports and feature requests, please file an issue at:
    https://github.com/Project-MONAI/monai-deploy-app-sdk/issues/new/choose

To choose an alternative python executable, set the environmental variable, "MONAI_PY_EXE".
'
}
check() {
    local do_black_format="false"
    local do_black_fix="false"
    local do_isort_format="false"
    local do_isort_fix="false"
    local do_flake8_format="false"
    local do_pytype_format="false"
    local do_mypy_format="false"

    local num_workers=$(nproc)

    local arg

    # parse arguments
    while [ $# -gt 0 ]; do
        arg="$1"

        case ${arg} in
            --dryrun)
                DO_DRY_RUN="true"  # set to true to print commands to screen without running
            ;;
            -f|--codeformat)
                do_black_format="true"
                do_isort_format="true"
                do_flake8_format="true"
                do_pytype_format="true"
                do_mypy_format="true"
            ;;
            --black)
                do_black_format="true"
            ;;
            --autofix)
                do_isort_fix="true"
                do_black_fix="true"
                do_isort_format="true"
                do_black_format="true"
            ;;
            --isort)
                do_isort_format="true"
            ;;
            --flake8)
                do_flake8_format="true"
            ;;
            --pytype)
                do_pytype_format="true"
            ;;
            --mypy)
                do_mypy_format="true"
            ;;
            -j|--jobs)
                num_workers=$2
                shift
            ;;
            *)
                c_echo_err R "Incorrect command is provided (invalid arg: " B "${arg}" R ")."
                print_cmd_help_messages check
            ;;
        esac
        shift
    done

    c_echo B "DO_DRY_RUN:" W " ${DO_DRY_RUN}"
    c_echo B "do_black_format:" W " ${do_black_format}"
    c_echo B "do_black_fix:" W " ${do_black_fix}"
    c_echo B "do_isort_format:" W " ${do_isort_format}"
    c_echo B "do_isort_fix:" W " ${do_isort_fix}"
    c_echo B "do_flake8_format:" W " ${do_flake8_format}"
    c_echo B "do_pytype_format:" W " ${do_pytype_format}"
    c_echo B "do_mypy_format:" W " ${do_mypy_format}"

    check_import "monai.deploy"

    if [ "${do_isort_format}" = "true" ]; then
        if [ "${do_isort_fix}" = "true" ]; then
            isort_format "fix"
        else
            isort_format "check"
        fi
    fi

    if [ "${do_black_format}" = "true" ]; then
        if [ "${do_black_fix}" = "true" ]; then
            black_format "fix"
        else
            black_format "check"
        fi
    fi

    if [ "${do_flake8_format}" = "true" ]; then
        flake8_format
    fi

    if [ "${do_pytype_format}" = "true" ]; then
        pytype_format "${num_workers}"
    fi

    if [ "${do_mypy_format}" = "true" ]; then
        mypy_format
    fi
}

pytest_desc() { c_echo 'Run tests with pytest

It executes the following command:

  ' "${MONAI_PY_EXE}"' -m pytest --cache-clear -vv \
      --cov=monai \
      --junitxml="'"$TOP/junit-monai-deploy-app-sdk.xml"'" \
      --cov-config="'"$TOP/.coveragerc"'" \
      --cov-report=xml:"'"$TOP/monai-deploy-app-sdk-coverage.xml"'" \
      --cov-report term \
      "$@"

Examples:
  ./run pytest tests/unit

Arguments:
  $@ - arguments to pass to pytest
'
}
pytest() {
    local result=0

    pushd $TOP > /dev/null
    run_command ${MONAI_PY_EXE} -m pytest --cache-clear -vv \
        --cov=monai \
        --junitxml="$TOP/junit-monai-deploy-app-sdk.xml" \
        --cov-config="$TOP/.coveragerc" \
        --cov-report=xml:"$TOP/monai-deploy-app-sdk-coverage.xml" \
        --cov-report term \
        "$@"
    result=$?
    popd > /dev/null

    return ${result}
}

test_desc() { c_echo 'Execute test cases

Arguments:
  $1 - subcommand [all] (default: all)
  $2 - test_type [all|unit|integration|system|performance] (default: all)
  $3 - test_component [all] (default: all)
'
}
test() {
    local subcommand="${1:-all}"
    local test_type="${2:-all}"
    shift;

    if [ "$subcommand" = "all" ] || [ "$subcommand" = "python" ]; then
        test_python "$@"
    fi
}

test_python() {
    local test_type="${1:-all}"
    local test_component="${2:-all}"
    local result=0

    local testsuite=""
    local testsuite_unit="tests/unit"
    local testsuite_integration="tests/integration"
    local testsuite_performance="tests/performance"
    local testsuite_system="tests/system"

    install_python_dev_deps

    if [ "$test_type" = "all" ] || [ "$test_type" = "unit" ]; then
        local testsuite="${testsuite_unit}"
    fi
    if [ "$test_type" = "all" ] || [ "$test_type" = "integration" ]; then
        local testsuite="${testsuite} ${testsuite_integration}"
    fi
    if [ "$test_type" = "all" ] || [ "$test_type" = "performance" ]; then
        testsuite="${testsuite} ${testsuite_performance}"
    fi
    if [ "$test_type" = "all" ] || [ "$test_type" = "system" ]; then
        testsuite="${testsuite} ${testsuite_system}"
    fi

    pytest ${testsuite}
    result=$?

    return $result
}


#==================================================================================
# Section: Example
#==================================================================================


#==================================================================================
# Section: Documentation
#==================================================================================

install_doc_requirements() {
    if [ -n "${VIRTUAL_ENV}" ] || [ -n "${CONDA_PREFIX}" ]; then
        run_command ${MONAI_PY_EXE} -m pip install -q -U setuptools pip wheel build
        run_command ${MONAI_PY_EXE} -m pip install -q -r ${TOP}/docs/requirements.txt
        install_edit_mode
        hash -r  # reload hash for sphinx-build command
    else
        c_echo_err R "You must be in a virtual environment to install dependencies."
        if [ ! -e "$TOP/.venv/dev/bin/python3" ]; then
            c_echo_err W "Installing a virtual environment at " G "$TOP/.venv/dev" W " ..."
            run_command ${MONAI_PY_EXE} -m venv "$TOP/.venv/dev"
        fi

        c_echo_err W "Please activate the virtual environment at " G "$TOP/.venv/dev" W " and execute the command again."
        c_echo_err
        c_echo_err G "  source $TOP/.venv/dev/bin/activate"
        c_echo_err G "  $0 $CMD $ARGS"
        exit 1
    fi
}

setup_gen_docs() {
    local output_folder=${1:-${TOP}/dist/docs}

    # Remove existing files in dist/docs
    run_command rm -rf ${output_folder}/*
    # Remove existing _autosummary folder
    run_command rm -rf ${TOP}/docs/source/modules/_autosummary

    # Symbolic link notebooks folder from 'docs/source/notebooks' to 'notebooks'
    run_command rm -rf ${TOP}/docs/source/notebooks
    run_command ln -sfn ${TOP}/notebooks ${TOP}/docs/source/notebooks
    # Symbolic link notebooks folder from 'docs/source/notebooks' to '_static/notebooks'
    run_command rm -rf ${TOP}/docs/_static/notebooks
    run_command ln -sfn ${TOP}/notebooks ${TOP}/docs/_static/notebooks
}

gen_docs_desc() { c_echo 'Generate documents

Generated docs would be available at ${TOP}/dist/docs.

Arguments:
  $1 - output folder path (html docs)

Returns:
  None

  Exit code:
    exit code returned from generating document
'
}
gen_docs() {
    local output_folder=${1:-${TOP}/dist/docs}
    local ret=0

    # Install prerequisites
    install_doc_requirements

    setup_gen_docs ${output_folder}

    c_echo W "Sphinx build html..."
    sphinx-build -E -b html $TOP/docs/source ${output_folder}
    ret=$?

    if [ $ret -eq 0 ]; then
        c_echo W "Sphinx linkcheck..."
        sphinx-build -b linkcheck $TOP/docs/source ${output_folder}
        ret=$?
    fi

    # Remove jupyter_execute folder explicitly until the issue is solved
    #   https://github.com/executablebooks/MyST-NB/issues/129
    rm -rf $(dirname ${output_folder})/jupyter_execute

    return $ret
}

gen_docs_dev_desc() { c_echo 'Generate documents (with dev-server)

Launch dev-server for sphinx.

Generated docs would be available at ${TOP}/dist/docs.

Arguments:
  -p <port> - port number
  -h <host> - hostname to serve documentation on (default: 0.0.0.0)
  -o <path> - output folder path (html docs)

Returns:
  None

  Exit code:
    exit code returned from generating document
'
}
gen_docs_dev() {
    local ret=0
    local OPTIND
    local output_folder=${TOP}/dist/docs
    local port=$(get_unused_ports 1 10001 10010)
    local host=0.0.0.0

    while getopts 'p:h:o:' option;
    do
        case "${option}" in
            p)
                port="$OPTARG"
                ;;
            h)
                host="$OPTARG"
                ;;
            o)
                output_folder="$OPTARG"
                ;;
            *)
                echo_err R "Invalid option!"
                return 1
        esac
    done
    shift $((OPTIND-1))

    # Install prerequisites
    install_doc_requirements

    setup_gen_docs ${output_folder}

    c_echo W "Sphinx build html..."
    sphinx-build -E -b html $TOP/docs/source ${output_folder}
    ret=$?

    if [ $ret -eq 0 ]; then
        c_echo W "Sphinx autobuild... Check " Y "http://localhost:${port}"
        run_command sphinx-autobuild --host ${host} --port ${port} ${TOP}/docs/source ${output_folder}
    fi
}

clean_docs_desc() { c_echo 'Clean up document-related files
'
}
clean_docs() {
    c_echo W 'Cleaning up document-related files...'
    run_command rm -rf ${TOP}/docs/source/modules/_autosummary
    run_command rm -rf ${TOP}/docs/notebooks
    run_command rm -rf ${TOP}/dist/docs
}

#==================================================================================
# Section: Release
#==================================================================================

bump_version_desc() { c_echo 'Bump version

Executes bump2version(https://github.com/c4urself/bump2version).
`bump2version` package would be installed if not available.

<part>
  - major   : a non-negative integer
  - minor   : a non-negative integer
  - patch   : a non-negative integer
  - release : "a", "b" or "rc"
  - build   : a positive integer

  e.g.)
    0.1.0a1
      major   : 0
      minor   : 1
      patch   : 0
      release : a
      build   : 1

Examples:
  ./run bump_version build    # 0.1.0a1 -> 0.1.0a2
  ./run bump_version release  # 0.1.0a1 -> 0.1.0b1
  ./run bump_version patch    # 0.1.0a1 -> 0.1.1
  ./run bump_version minor    # 0.1.0a1 -> 0.2.0

Arguments:
  $1 - part (major, minor, patch, release, build)
  $@ - additional arguments for bump2version

Returns:
  Outputs executed by bump2version

  Exit code:
    exit code returned from bump2version
'
}
bump_version() {
    local part=${1:-}
    local ret=0

    if ! command -v bump2version > /dev/null; then
        c_echo G "bump2version" W " doesn't exists. Installing prerequisites..."
        install_python_dev_deps
    fi

    pushd $TOP > /dev/null

    case "$part" in
        major|minor|patch|release|build)
            local current_version="$(bump2version --dry-run --list --allow-dirty $part | grep -Po 'current_version=\K[\d\.]+((a|b|rc)\d+)?')"
            local new_version="$(bump2version --dry-run --list --allow-dirty $part | grep -Po 'new_version=\K[\d\.]+((a|b|rc)\d+)?')"
            c_echo W "current_version=" G "${current_version}"
            c_echo W "new_version=" G "${new_version}"

            bump2version "$@"
            ret=$?
            ;;
        *)
            bump2version "$@"
            ret=$?
            ;;
    esac

    popd > /dev/null

    return $ret
}

#==================================================================================

parse_args() {
    local OPTIND
    while getopts 'yh' option;
    do
        case "${option}" in
            y)
                ALWAYS_YES="true"
                ;;
            h)
                print_usage
                exit 1
                ;;
            *)
                ;;
        esac
    done
    shift $((OPTIND-1))

    CMD="$1"
    shift

    ARGS=("$@")
    # Check if the command has `--help` or `-h` and override the CMD
    local arg
    for arg in "${ARGS[@]}"; do
        if [ "$arg" = "--help" ] || [ "$arg" = "-h" ]; then
            ARGS=("$CMD")
            CMD="help"
            break
        fi
    done
}

print_usage() {
    set +x
    echo_err
    echo_err "USAGE: $0 [command] [arguments]..."
    echo_err ""
    c_echo_err W "Global Arguments"
    echo_err
    c_echo_err W "Command List"
    c_echo_err Y "    help  " w "----------------------------  Print detailed description for a given argument (command name)"
    echo_err "$(get_list_of_available_commands color "${RUN_SCRIPT_FILE}" | my_cat_prefix " ")"
    echo_err
}

print_cmd_help_messages() {
    local cmd="$1"
    if [ -n "${cmd}" ]; then
        if type ${cmd}_desc > /dev/null 2>&1; then
            ${cmd}_desc
            exit 0
        else
            c_echo_err R "Command '${cmd}' doesn't exist!"
            exit 1
        fi
    fi
    print_usage
    return 0
}

main() {
    local ret=0
    parse_args "$@"

    case "$CMD" in
        help)
            print_cmd_help_messages "${ARGS[@]}"
            exit 0
            ;;
        ''|main)
            print_usage
            ;;
        *)
            if type ${CMD} > /dev/null 2>&1; then
                "$CMD" "${ARGS[@]}"
            else
                print_usage
                exit 1
            fi
            ;;
    esac
    ret=$?
    if [ -n "${SCRIPT_DIR}" ]; then
        exit $ret
    fi
}

init_globals

if [ -n "${SCRIPT_DIR}" ]; then
    main "$@"
fi


# Description template

# Globals:
#   MONAI_OS
#   MONAI_TARGET
#   MONAI_USER (used if MONAI_OS is "linux")
#   MONAI_HOST (used if MONAI_OS is "linux")
# Arguments:
#   Command line to execute
# Returns:
#   Outputs print messages during the execution (stdout->stdout, stderr->stderr).

#   Note:
#     This command removes "\r" characters from stdout.

#   Exit code:
#     exit code returned from executing a given command
